<!--
 * @Author: yehongbo 1394168736@qq.com
 * @Date: 2024-02-06 11:02:19
 * @LastEditors: yehongbo 1394168736@qq.com
 * @LastEditTime: 2024-03-01 19:53:37
 * @FilePath: \giteeDownload\testthree\src\views\test1.vue
 * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
  <!-- <div id="container">
  </div> -->
</template>

<script setup>
import * as THREE from 'three'
import { onMounted } from 'vue';
// 引入轨道控制器扩展库OrbitControls.js
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
//引入性能监视器stats.js
import Stats from 'three/addons/libs/stats.module.js';
// 引入dat.gui.js的一个类GUI
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';

// 实例化一个gui对象
const gui = new GUI();
//改变交互界面style属性
gui.domElement.style.right = '0px';
gui.domElement.style.width = '300px';

//创建一个对象，对象属性的值可以被GUI库创建的交互界面改变
// const obj = {
//   x: 30,
// };
// // gui增加交互界面，用来改变obj对应属性
// gui.add(obj, 'x', 0, 100);
// setInterval(function () {
//   console.log('x', obj.x);
// }, 1000)

const scene = new THREE.Scene()

//创建一个长方体几何对象Geometry
// //BoxGeometry：长方体
var group = new THREE.Group(); // 创建一个空的Group对象
for (let i = 0; i < 5; i++) {
  for (let j = 0; j < 5; j++) {
    for (let z = 0; z < 5; z++) {
      const geometry = new THREE.SphereGeometry(10);
      const material = new THREE.MeshLambertMaterial({
        color: 0x00ffff
      });
      const mesh = new THREE.Mesh(geometry, material);
      mesh.position.x = i * 100; // 网格模型mesh沿着x轴方向阵列
      mesh.position.y = j * 100; // 网格模型mesh沿着x轴方向阵列
      mesh.position.z = z * 100; // 网格模型mesh沿着x轴方向阵列
      group.add(mesh); //添加到组对象group1
      mesh.name = i + 1 + '号楼';
      // console.log('mesh.name',mesh.name);
    }
  }

}
scene.add(group)
// // BoxGeometry:矩形
// const geometry = new THREE.BoxGeometry(50);
// // SphereGeometry：球体
// const geometry = new THREE.SphereGeometry(50);
// // CylinderGeometry：圆柱
// const geometry = new THREE.CylinderGeometry(50, 50, 100);
// // PlaneGeometry：矩形平面
// const geometry = new THREE.PlaneGeometry(100, 50);
// // CircleGeometry：圆形平面
// const geometry = new THREE.CircleGeometry(50);

// 双面可见

// Three.js的材质默认正面可见，反面不可见, 对于矩形平面PlaneGeometry、圆形平面如果你想看到两面，可以设置side: THREE.DoubleSide。

// new THREE.MeshBasicMaterial({
//   side: THREE.FrontSide, //默认只有正面可见
// });

// new THREE.MeshBasicMaterial({
//   side: THREE.DoubleSide, //两面可见
// });


//创建一个材质对象Material
// 基础材质 不收光照影响
// const material = new THREE.MeshBasicMaterial({
//   color: 0x0000ff, //设置材质颜色
//   transparent: true,//开启透明
//   opacity: 0.5,//设置透明度
// });

// 漫反射 均匀反射
// const material = new THREE.MeshLambertMaterial({
//   color: 0x0000ff, //设置材质颜色
//   transparent: true,//开启透明
//   opacity: 0.5,//设置透明度
// });

// 高光反射 在特定角度和位置，你可以看到车表面某个局部区域非常高亮
// const material = new THREE.MeshPhongMaterial({
//   color: 0x0000ff, //设置材质颜色
//   transparent: true,//开启透明
//   opacity: 0.5,//设置透明度
//   shininess: 20, //高光部分的亮度，默认30
//   specular: 0xffffff, //高光部分的颜色
// });

// const mesh = new THREE.Mesh(geometry, material); //网格模型对象Mesh
// // mesh.interactable = true; //设置模型可交互
// // 在XOZ平面上分布
// mesh.position.set(0, 0, 0);
// scene.add(mesh); //网格模型添加到场景中  
// let mesh = ''
// for (let i = 0; i < 10; i++) {
//   for (let j = 0; j < 10; j++) {
//     mesh = new THREE.Mesh(geometry, material); //网格模型对象Mesh
//     // 在XOZ平面上分布
//     mesh.position.set(i * 100, 0, j * 100);
//     scene.add(mesh); //网格模型添加到场景中  
//   }
// }
// const mesh = new THREE.Mesh(geometry, material); //网格模型对象Mesh
// //设置网格模型在三维空间中的位置坐标，默认是坐标原点
// mesh.position.set(50, 0, 0);
// scene.add(mesh)

//点光源：两个参数分别表示光源颜色和光照强度
// 参数1：0xffffff是纯白光,表示光源颜色
// 参数2：1.0,表示光照强度，可以根据需要调整
const pointLight = new THREE.PointLight(0xffffff, 1.0);
pointLight.decay = 0.0;//设置光源不随距离衰减
//点光源位置
pointLight.position.set(200, 200, 300);//点光源放在x轴上
scene.add(pointLight); //点光源添加到场景中

//环境光:没有特定方向，整体改变场景的光照明暗
const ambient = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambient);

// 平行光
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
// 设置光源的方向：通过光源position属性和目标指向对象的position属性计算
directionalLight.position.set(-80, -100, -50);
// 方向光指向对象网格模型mesh，可以不设置，默认的位置是0,0,0
directionalLight.target = group;
scene.add(directionalLight);
// DirectionalLightHelper：可视化平行光
const dirLightHelper = new THREE.DirectionalLightHelper(directionalLight, 5, 0xff0000);
scene.add(dirLightHelper);



// 点光源辅助可视化观察
const pointLightHelper = new THREE.PointLightHelper(pointLight, 10);
scene.add(pointLightHelper);


// AxesHelper：辅助观察的坐标系
const axesHelper = new THREE.AxesHelper(150);
scene.add(axesHelper);

const obj2 = {
  bool: false,
};

const width1 = 800; //宽度
const height1 = 500; //高度
// 实例化一个透视投影相机对象
// 30:视场角度, width / height:Canvas画布宽高比, 1:近裁截面, 3000：远裁截面
// // 超出视锥体远裁界面的范围的会被剪裁掉，不渲染  可以调整far参数适配 所以要看全部就把远裁面拉高10000
const camera = new THREE.PerspectiveCamera(30, width1 / height1, 1, 10000);

//相机在Three.js三维坐标系中的位置
// 根据需要设置相机位置具体值
camera.position.set(500, 500, 500);
// camera.lookAt(mesh.position);//指向mesh对应的位置
// // 改变相机观察目标点
camera.lookAt(250, 0, 250);

// width和height用来设置Three.js输出的Canvas画布尺寸(像素px)
// const width = 800; //宽度
// const height = 500; //高度
const width = window.innerWidth; //宽度
const height = window.innerHeight; //高度
// 创建渲染器对象
// 设置渲染器锯齿属性.antialias的值可以直接在参数中设置，也可通过渲染器对象属性设置
const renderer = new THREE.WebGLRenderer({
  antialias: true,
});
// 或者直接设置参数  renderer.antialias = true


// 不同硬件设备的屏幕的设备像素比window.devicePixelRatio值可能不同
console.log('查看当前屏幕设备像素比', window.devicePixelRatio);

// 如果你遇到你的canvas画布输出模糊问题，注意设置renderer.setPixelRatio(window.devicePixelRatio)。
// 注意：注意你的硬件设备设备像素比window.devicePixelRatio刚好是1，那么是否执行.setPixelRatio()不会有明显差异，不过为了适应不同的硬件设备屏幕，通常需要执行该方法。
// 获取你屏幕对应的设备像素比.devicePixelRatio告诉threejs,以免渲染模糊问题
renderer.setPixelRatio(window.devicePixelRatio);

renderer.setClearColor(0x444444, 1); //设置背景颜色

renderer.setSize(width, height); //设置three.js渲染区域的尺寸(像素px)
renderer.render(scene, camera); //执行渲染操作
onMounted(() => {
  // document.getElementById('container').appendChild(renderer.domElement);
  document.body.appendChild(renderer.domElement);
})

// 设置相机控件轨道控制器OrbitControls
const controls = new OrbitControls(camera, renderer.domElement);

// // 改变相机观察目标点 camera.lookAt(250, 0, 250);
// 意相机控件OrbitControls会影响lookAt设置，注意手动设置OrbitControls的目标参数
controls.target.set(250, 0, 250)
controls.update()


// 如果OrbitControls改变了相机参数，重新调用渲染器渲染三维场景
controls.addEventListener('change', function () {
  // 浏览器控制台查看相机位置变化
  // console.log('camera.position', camera.position);
  // 渲染循环和相机控件OrbitControls

  // 设置了渲染循环, 相机控件OrbitControls就不用再通过事件change执行renderer.render(scene, camera);，毕竟渲染循环一直在执行renderer.render(scene, camera);。
  // renderer.render(scene, camera); //执行渲染操作
});//监听鼠标、键盘事件

// 渲染函数
// threejs可以借助HTML5的API请求动画帧window.requestAnimationFrame实现动画渲染
// 备注说明：对于部分高刷新率的电脑硬件设备，.requestAnimationFrame每秒钟默认调用函数执行次数也是有可能超过60次的，比如你的电脑显卡、显示器等硬件能够支持144hz刷新频率，.requestAnimationFrame的每秒执行上限，也可以接近144帧率
// 渲染循环
const clock = new THREE.Clock();
//创建stats对象
const stats = new Stats();
//stats.domElement:web页面上输出计算结果,一个div元素，
document.body.appendChild(stats.domElement);


// 创建射线对象Ray
const ray = new THREE.Ray()
// 射线起点.origin

// 射线Ray的起点.origin在3D空间中的坐标，可以用一个三维向量Vector3的x、y、z分量表示。

// 设置射线起点
ray.origin = new THREE.Vector3(1, 0, 3);

// 起点.origin属性值是三维向量Vector3，也可以用.set()方法设置。

// // 设置射线起点
// ray.origin.set(1, 0, 3);
// 射线方向.direction

// 射线Ray的方向.direction通用用一个三维向量Vector3表示, 向量长度保证为1，也就是单位向量。

// 表示射线沿着x轴正方向
ray.direction = new THREE.Vector3(1, 0, 0);
// 表示射线沿着x轴负方向
// ray.direction = new THREE.Vector3(-1, 0, 0);

// // 表示射线沿着y方向
// ray.direction = new THREE.Vector3(0, 1, 0);

// 注意.direction的值需要是单位向量，不是的话可以执行.normalize()归一化或者说标准化。

// ray.direction = new THREE.Vector3(5, 0, 0).normalize();

// // 表示射线沿着xy坐标轴的中间线
// ray.direction = new THREE.Vector3(1, 1, 0).normalize();

// #
// 射线投射器Raycaster

// 射线投射器Raycaster具有一个射线属性.ray，该属性的值就是上节课讲解的射线对象Ray。

// const raycaster = new THREE.Raycaster();
// console.log('射线属性', raycaster.ray);

// // 设置射线起点
// raycaster.ray.origin = new THREE.Vector3(-100, 0, 0);
// // 设置射线方向射线方向沿着x轴
// raycaster.ray.direction = new THREE.Vector3(1, 0, 0);
// 射线交叉计算(.intersectObjects()方法)

// 射线投射器Raycaster通过.intersectObjects()方法可以计算出来与自身射线.ray相交的网格模型。

// .intersectObjects([mesh1, mesh2, mesh3])对参数中的网格模型对象进行射线交叉计算, 未选中对象返回空数组[], 选中一个对象，数组1个元素，选中多个对象，数组多个元素，如果选中多个对象，对象在数组中按照先后排序。

// 如果OrbitControls改变了相机参数，重新调用渲染器渲染三维场景

renderer.domElement.addEventListener('mousemove', function (event) {
  // .offsetY、.offsetX以canvas画布左上角为坐标原点,单位px
  const px = event.offsetX;
  const py = event.offsetY;
  //屏幕坐标px、py转WebGL标准设备坐标x、y
  //width、height表示canvas画布宽高度
  const x = (px / width) * 2 - 1;
  const y = -(py / height) * 2 + 1;
  //创建一个射线投射器`Raycaster`
  const raycaster = new THREE.Raycaster();
  //.setFromCamera()计算射线投射器`Raycaster`的射线属性.ray
  // 形象点说就是在点击位置创建一条射线，射线穿过的模型代表选中
  raycaster.setFromCamera(new THREE.Vector2(x, y), camera);
  //.intersectObjects([mesh1, mesh2, mesh3])对参数中的网格模型对象进行射线交叉计算
  // 未选中对象返回空数组[],选中一个对象，数组1个元素，选中两个对象，数组两个元素
  const intersects = raycaster.intersectObjects([group]);
  console.log("射线器返回的对象", intersects);
  // intersects.length大于0说明，说明选中了模型
  if (intersects.length > 0) {
    // 选中模型的第一个模型，设置为红色
    intersects[0].object.material.color.set(0xff0000);
  }
}, false)


function render() {
  // const spt = clock.getDelta() * 1000;//毫秒
  // console.log('两帧渲染时间间隔(毫秒)', spt);
  // console.log('帧率FPS', 1000 / spt);
  stats.update();
  renderer.render(scene, camera); //执行渲染操作
  // if (obj2.bool) mesh.rotateY(0.01); // GUI进行单选框控制是否旋转
  // mesh.rotateY(0.01);//每次绕y轴旋转0.01弧度
  requestAnimationFrame(render);//请求再次执行渲染函数render，渲染下一帧
}
render();
// onresize 事件会在窗口被调整大小时发生
window.onresize = function () {
  // 重置渲染器输出画布canvas尺寸
  renderer.setSize(window.innerWidth, window.innerHeight);
  // 全屏情况下：设置观察范围长宽比aspect为窗口宽高比
  camera.aspect = window.innerWidth / window.innerHeight;
  // 渲染器执行render方法的时候会读取相机对象的投影矩阵属性projectionMatrix
  // 但是不会每渲染一帧，就通过相机的属性计算投影矩阵(节约计算资源)
  // 如果相机的一些属性发生了变化，需要执行updateProjectionMatrix ()方法更新相机的投影矩阵
  camera.updateProjectionMatrix();
};

// .add()创建的交互界面，会默认显示所改变属性的名字，为了通过交互界面更好理解你改变的某个对象属性，
// 你可以通过.name()方法改变gui生成交互界面显示的内容
// 步长.step()方法可以设置交互界面每次改变属性值间隔是多少。
// 当gui界面某个值的时候，.onChange()方法就会执行，这时候你可以根据需要通过.onChange()执行某些代码。
// .addColor()生成颜色值改变的交互界面
gui.add(camera.position, 'x', 0, 1000).name('相机x').step(1).onChange(function (value) {
  // mesh.position.x = value;
  // 你可以写任何你想跟着obj.x同步变化的代码
  // 比如mesh.position.y = value;
  if (value > 800) {
    console.log(`${value}太大了`)
  }

});

// 分组
const matFolder = gui.addFolder('材质1');
matFolder.add(camera.position, 'y', 0, 1000).name('相机y').step(1);
// 参数3和参数4，分别是一个数字，交互界面是一个鼠标可以拖动的拖动条，可以在一个区间改变属性的值
matFolder.add(camera.position, 'z', 0, 1000).name('相机z').step(1);
matFolder.close()
// 参数3是一个数组，生成交互界面是下拉菜单
const matFolder1 = gui.addFolder('材质2');
matFolder1.add(camera.position, 'z', [0, 500, 1000]).name('相机z').step(1);
// 参数3是一个对象，生成交互界面是下拉菜单   变成有tip的选择
matFolder1.add(camera.position, 'z', {
  left: 0, center: 500, right: 1000, top: 1200
  // 左: -100,//可以用中文
  // 中: 0,
  // 右: 100
}).name('相机z').step(1);

// 如果.add()改变属性的对应的数据类型如果是布尔值，那么交互界面就是一个单选框。

// 改变的obj属性数据类型是布尔值，交互界面是单选框
matFolder1.add(obj2, 'bool').name('是否旋转');

matFolder1.close()
const obj1 = {
  color: 0x00ffff,
};

// 子分组
const dirFolder2 = matFolder1.addFolder('位置');//子菜单的子菜单
dirFolder2.close();//关闭菜单
// 平行光位置
dirFolder2.add(directionalLight.position, 'x', -400, 400);
dirFolder2.add(directionalLight.position, 'y', -400, 400);
dirFolder2.add(directionalLight.position, 'z', -400, 400);

// .addColor()生成颜色值改变的交互界面
// gui.addColor(obj1, 'color').onChange(function (value) {
//   mesh.material.color.set(value);
// });



</script>
<style scoped>
#container {
  width: 500px;
  height: 500px;
  margin: 100px auto;
  background: gray;
}
</style>